package render;

import math.Maths;
import math.Vecmath;
import math.vec3f;
import util.Color;

public class RenderEngine {
    
    private final int TRI_LIMIT;
    
    private final float Nz;
    private final float Fz;
    private final float Rx;
    private final float Lx;
    private final float Uy;
    private final float Dy;
    
    private final float RIGHT;
    private final float LEFT;
    private final float UP;
    private final float DOWN;
    private final float FAR;
    private final float NEAR;
    
    private final vec3f near;
    private final vec3f far;
    private final vec3f right;
    private final vec3f left;
    private final vec3f up;
    private final vec3f down;
    
    private final vec3f ptNear;
    private final vec3f ptFar;
    
    private Rasterizer _raster;
    private Tri3[] tris;
    private vec3f rotate;
    private vec3f translate;
    private int indTris;
    
    public RenderEngine(Rasterizer _rasterizer) {
        _raster = _rasterizer;
        TRI_LIMIT = 1000;
        
        Nz = -5.0f;
        Fz = -55.0f;
        Rx = 50.0f;
        Lx = -Rx;
        Uy = 30.0f;
        Dy = -Uy;
        
        RIGHT = 0;
        LEFT = 1;
        UP = 2;
        DOWN = 3;
        FAR = 4;
        NEAR = 5;
        // All plane normals are facing INWARDS at the moment
        near = new vec3f(0, 0, -1);
        far = new vec3f(0, 0, 1);
        right = (new vec3f(Fz, -Rx, 0).getUnitVectorCopy());
        left = (new vec3f(-Fz, Lx, 0).getUnitVectorCopy());
        up = (new vec3f(0, Fz, -Uy).getUnitVectorCopy());
        down = (new vec3f(0, -Fz, Dy).getUnitVectorCopy());
        
        ptNear = new vec3f(0, 0, Nz);
        ptFar = new vec3f(0, 0, Fz);
        
        tris = new Tri3[TRI_LIMIT];
        rotate = new vec3f();
        translate = new vec3f();
        indTris = 0;
    }
    
    public void render() {
        _raster.clear(Color.BLUE); // nPixels iter
        _raster.clearZBuffer(); // nPixels iter
        
        coordWorldToEye(); // nTris iter <=> 2 * nVertices iter
        
        clipTriangles(); // nTris iter <=> nVertices iter
        
        coordEyeToNorm(); // nTris iter <=> nVertices iter
        coordNormToScreen(); // nTris iter <=> nVertices iter
        
        rasterTris(); // nTris iter <=> varied nPixels per tri
        
        clearTris(); // nTris iter
    }
    
    private void coordWorldToEye() { // World coordinates -> Eye coordinates
        for (int i = 0; i < indTris; i++) {
            tris[i].translate(translate);
            tris[i].rotateRad(rotate, 0f, 0f, 0f);
        }
    }
    
    private void clipTriangles() {
        for (int i = 0; i < indTris; i++) {
            boolean[] ps0 = insideFrustumAll(tris[i].verts[0]);
            boolean[] ps1 = insideFrustumAll(tris[i].verts[1]);
            boolean[] ps2 = insideFrustumAll(tris[i].verts[2]);
            for (int j = 0; j < 6; j++) { // Iterate through all clipping planes
                if (ps0[j] && ps1[j] && ps2[j]) { // If all pass the j'th plane
                    continue; // Continue on to the next j+1 plane
                } else if (!ps0[j] && !ps1[j] && !ps2[j]) { // If all fail the j'th plane
                    tris[i] = null; // Triangle is outside viewport, so delete it
                    break; // Stop checking clipping planes for the deleted triangle
                } else {
                    if (ps0[j] && !ps1[j] && !ps2[j]) { // One passes the j'th plane, modified Tri3
                        clipTri1In2Out(i, j, 0, 1, 2);
                    } else if (!ps0[j] && ps1[j] && !ps2[j]) { // One passes the j'th plane, modified Tri3
                        clipTri1In2Out(i, j, 1, 0, 2);
                    } else if (!ps0[j] && !ps1[j] && ps2[j]) { // One passes the j'th plane, modified Tri3
                        clipTri1In2Out(i, j, 2, 0, 1);
                    } else if (ps0[j] && ps1[j] && !ps2[j]) { // Two pass the j'th plane, new Tri3 generated
                        clipTri2In1Out(i, j, 0, 1, 2);
                    } else if (ps0[j] && !ps1[j] && ps2[j]) { // Two pass the j'th plane, new Tri3 generated
                        clipTri2In1Out(i, j, 0, 2, 1);
                    } else if (!ps0[j] && ps1[j] && ps2[j]) { // Two pass the j'th plane, new Tri3 generated
                        clipTri2In1Out(i, j, 1, 2, 0);
                    }
                }
            }
        }
    }
    
    private void clipTri1In2Out(int triIndex, int planeIndex, int v0ind, int v1ind, int v2ind) {
        vec3f plNorm = getPlaneNorm(planeIndex);
        vec3f plPt = getPlanePt(planeIndex);
        vec3f v01 = Vecmath.subtract(tris[triIndex].verts[v1ind], tris[triIndex].verts[v0ind]);
        vec3f v02 = Vecmath.subtract(tris[triIndex].verts[v2ind], tris[triIndex].verts[v0ind]);
        float t01 = (Vecmath.dotProduct(plNorm, Vecmath.subtract(plPt, tris[triIndex].verts[v0ind])))
                / (Vecmath.dotProduct(plNorm, v01));
        if (!((Float)t01).isInfinite() && t01 >= 0.0f && t01 <= 1.0f) {
            if (tris[triIndex].hasTexCoords()) {
                tris[triIndex].texs[v1ind].x = Maths.lerp(t01, tris[triIndex].texs[v0ind].x, tris[triIndex].texs[v1ind].x);
                tris[triIndex].texs[v1ind].y = Maths.lerp(t01, tris[triIndex].texs[v0ind].y, tris[triIndex].texs[v1ind].y);
            }
            tris[triIndex].verts[v1ind] = Vecmath.add(Vecmath.scalarMult(t01, v01), tris[triIndex].verts[v0ind]);
        }
        float t02 = (Vecmath.dotProduct(plNorm, Vecmath.subtract(plPt, tris[triIndex].verts[v0ind])))
                / (Vecmath.dotProduct(plNorm, v02));
        if (!((Float)t02).isInfinite() && t02 >= 0.0f && t02 <= 1.0f) {
            if (tris[triIndex].hasTexCoords()) {
                tris[triIndex].texs[v2ind].x = Maths.lerp(t02, tris[triIndex].texs[v0ind].x, tris[triIndex].texs[v2ind].x);
                tris[triIndex].texs[v2ind].y = Maths.lerp(t02, tris[triIndex].texs[v0ind].y, tris[triIndex].texs[v2ind].y);
            }
            tris[triIndex].verts[v2ind] = Vecmath.add(Vecmath.scalarMult(t02, v02), tris[triIndex].verts[v0ind]);
        }
    }
    
    private void clipTri2In1Out(int triIndex, int planeIndex, int v0ind, int v1ind, int v2ind) {
        
    }
    
    private void coordEyeToNorm() { // Eye coordinates -> Normalized coordinates
        float localRx;
        float localUy;
        for (int i = 0; i < indTris; i++) {
            if (tris[i] != null) {
                for (int j = 0; j < Tri3.nVerts; j++) {
                    localRx = Maths.lerpf(tris[i].verts[j].z, Fz, Nz, Rx, 0);
                    localUy = Maths.lerpf(tris[i].verts[j].z, Fz, Nz, Uy, 0);
                    tris[i].verts[j].x /= localRx;
                    tris[i].verts[j].y /= localUy;
                    tris[i].verts[j].z = Maths.lerpf(tris[i].verts[j].z, Fz, Nz, -1.0f, 1.0f);
                }
            }
        }
    }
    
    private void coordNormToScreen() { // Normalized coordinates -> Screen coordinates
        for (int i = 0; i < indTris; i++) {
            if (tris[i] != null) {
                for (int j = 0; j < Tri3.nVerts; j++) {
                    tris[i].verts[j].x = (tris[i].verts[j].x + 1.0f) * _raster.drawW * 0.5f;
                    tris[i].verts[j].y = _raster.drawH - (tris[i].verts[j].y + 1.0f) * _raster.drawH * 0.5f;
                }
            }
        }
    }
    
    private void rasterTris() {
        for (int i = 0; i < indTris; i++) {
            if (tris[i] != null) {
                tris[i].raster(_raster);
            }
        }
    }
    
    private void clearTris() {
        for (int i = 0; i < indTris; i++) {
            tris[i] = null;
        }
        indTris = 0;
    }
    
    private boolean[] insideFrustumAll(vec3f v) {
        return new boolean[] {
            insideFrustumR(v),
            insideFrustumL(v),
            insideFrustumU(v),
            insideFrustumD(v),
            insideFrustumF(v),
            insideFrustumN(v),
        };
    }
    
    private vec3f getPlaneNorm(int planeIndex) {
        if (planeIndex == RIGHT) {
            return right;
        } else if (planeIndex == LEFT) {
            return left;
        } else if (planeIndex == UP) {
            return up;
        } else if (planeIndex == DOWN) {
            return down;
        } else if (planeIndex == FAR) {
            return far;
        } else if (planeIndex == NEAR) {
            return near;
        } else { // Will never reach here.
            return null;
        }
    }
    
    private vec3f getPlanePt(int planeIndex) {
        if (planeIndex == RIGHT || planeIndex == LEFT || planeIndex == UP || planeIndex == DOWN) {
            return Vecmath.getOrigin();
        } else if (planeIndex == FAR) {
            return ptFar;
        } else if (planeIndex == NEAR) {
            return ptNear;
        } else { // Will never reach here.
            return null;
        }
    }
    
    private boolean insideFrustumR(vec3f v) {
        return v.x * (Fz / Rx) - v.z >= 0;
    }
    
    private boolean insideFrustumL(vec3f v) {
        return v.x * (Fz / Lx) - v.z >= 0;
    }
    
    private boolean insideFrustumU(vec3f v) {
        return v.z * (Uy / Fz) - v.y >= 0;
    }
    
    private boolean insideFrustumD(vec3f v) {
        return v.z * (Dy / Fz) - v.y <= 0;
    }
    
    private boolean insideFrustumF(vec3f v) {
        return v.z >= Fz;
    }
    
    private boolean insideFrustumN(vec3f v) {
        return v.z <= Nz;
    }
    
    
    
    public void triangle(Tri3 tri) {
        tris[indTris++] = tri;
    }
    
    public void rotate(float pitch, float yaw, float roll) {
        rotate.x = pitch;
        rotate.y = yaw;
        rotate.z = roll;
    }
    
    public void rotate(vec3f rotations) {
        rotate(rotations.x, rotations.y, rotations.z);
    }
    
    public void translate(float dx, float dy, float dz) {
        translate.x = dx;
        translate.y = dy;
        translate.z = dz;
    }
    
    public void translate(vec3f translation) {
        translate(translation.x, translation.y, translation.z);
    }
    
}
